package fxc.example;

import org.apache.commons.math3.distribution.NormalDistribution;
import org.apache.commons.math3.distribution.PoissonDistribution;
import org.apache.commons.math3.exception.MathIllegalArgumentException;
import org.apache.commons.math3.stat.Frequency;
import org.apache.commons.math3.stat.StatUtils;
import org.apache.commons.math3.stat.descriptive.moment.Kurtosis;
import org.apache.commons.math3.stat.descriptive.moment.Skewness;
import org.apache.commons.math3.util.FastMath;
import tech.tablesaw.plotly.Plot;
import tech.tablesaw.plotly.components.Axis;
import tech.tablesaw.plotly.components.Figure;
import tech.tablesaw.plotly.components.Layout;
import tech.tablesaw.plotly.components.Marker;
import tech.tablesaw.plotly.traces.BarTrace;
import tech.tablesaw.plotly.traces.HistogramTrace;

import java.util.*;

public class MonteCarloSimulation {
    private double [] dependentVariables;
    private double [][] variables;

    private Model model;
    private int numberOfTests;

    private final Map<String, Double> statMap = new LinkedHashMap<>();
    List mode;

    boolean showChart = false;

    public boolean isShowChart() {
        return showChart;
    }

    public void setShowChart(boolean showChart) {
        this.showChart = showChart;
    }


    boolean calcStat(double []dependentVariables){
        try {
            statMap.put("N", (double)dependentVariables.length);
            double baseCase = model.getBaseCase();
            statMap.put("baseCase", baseCase);
            double mean = StatUtils.mean(dependentVariables);
            statMap.put("mean", mean);
            double variance = StatUtils.variance(dependentVariables);
            statMap.put("variance", variance);
            double std = FastMath.sqrt(variance);
            statMap.put("std", std);
            double median = StatUtils.percentile(dependentVariables, 50);
            statMap.put("median", median);
            Frequency q = new Frequency();
            for (double s: dependentVariables){
                q.addValue(String.valueOf(s));
            }
            List mode = q.getMode();
            if (mode!=null&& !mode.isEmpty()) {
                if (mode.size()==1){
                    statMap.put("mode", (double)Double.valueOf((String) mode.get(0)));
                }else {
                    if (mode.size() < dependentVariables.length){
                        this.mode = mode;
                        statMap.put("mode", null);
                    }else {
                        this.mode = new ArrayList();
                    }

                }
            }

            Skewness skewness = new Skewness();
            double skewnessResult = skewness.evaluate(dependentVariables);
            statMap.put("skewness",  skewnessResult);
            Kurtosis kurtosis = new Kurtosis();
            double kurtosisResult = kurtosis.evaluate(dependentVariables);
            statMap.put("kurtosis", kurtosisResult);
            double CV = std/mean;
            statMap.put("CV", CV);
            double max = StatUtils.max(dependentVariables);
            statMap.put("max", max);
            double min = StatUtils.min(dependentVariables);
            statMap.put("min", min);
            double SEM = std/Math.sqrt(dependentVariables.length);
            statMap.put("SEM", SEM);
        } catch (MathIllegalArgumentException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    boolean simulation( int simulatSize){
        return simulation(null, null, simulatSize);
    }

    boolean simulation(boolean[] hasValue, double[]value, int simulatSize){
        try {
            if (hasValue!=null&&value!=null){
                if (hasValue.length!=value.length){
                    throw new RuntimeException("hasValue.length!=value.length");
                }
                if (hasValue.length!=model.variables.size()){
                    throw new RuntimeException("hasValue.length!=variables.length");
                }
            }

            numberOfTests = simulatSize;
            dependentVariables = new double[numberOfTests];
            variables = new double[getModel().variables.size()][];
            for (int i = 0; i< getModel().variables.size(); i++){
                if (hasValue!=null&&hasValue[i]&&value!=null){
                    variables[i] = new double[numberOfTests];
                    for (int j = 0; j < numberOfTests; j++) {
                        variables[i][j] = value[i];
                    }
                }else {
                    if (getModel().variables.get(i).getDistributionType().equals("normalDistribution")) {
                        NormalDistributionVariable variable = (NormalDistributionVariable) getModel().variables.get(i);
                        NormalDistribution normalDistribution = (NormalDistribution) variable.getDistribution();
                        variables[i] = normalDistribution.sample(numberOfTests);
                    } else if (getModel().variables.get(i).getDistributionType().equals("poissonDistribution")) {
                        PoissonDistributionVariable variable = (PoissonDistributionVariable) getModel().variables.get(i);
                        PoissonDistribution poissonDistribution = (PoissonDistribution) variable.getDistribution();
                        variables[i] = new double[numberOfTests];
                        int[] int_variables = poissonDistribution.sample(numberOfTests);
                        for (int j = 0; j < int_variables.length; j++) {
                            variables[i][j] = int_variables[j];
                        }
                    }
                }

            }
            for (int repeat = 0; repeat<numberOfTests;repeat++){
                double resultOnce = 0.0d;
                for (int i = 0; i< getModel().variables.size(); i++){
                    resultOnce+= getModel().betas.get(i)*variables[i][repeat];
                }
                resultOnce += getModel().residual;
                dependentVariables[repeat] = resultOnce;

            }
            this.calcStat(dependentVariables);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    public double[] getDependentVariables() {
        return dependentVariables;
    }

    public double[][] getVariables() {
        return variables;
    }

    public Model getModel() {
        return model;
    }

    public void setModel(Model model) {
        this.model = model;
    }

    public int getNumberOfTests() {
        return numberOfTests;
    }

    public void setNumberOfTests(int numberOfTests) {
        this.numberOfTests = numberOfTests;
    }

    public Map<String, Double> getStatMap() {
        return statMap;
    }

    private static class FrequencyData {
        Double[] binCenters;
        Double[] binLefts;
        Double[] binRights;
        int[] counts;
        double[] frequencies;

    }

    private static FrequencyData calculateFrequencyData(double[] data, int numBins) {
        double min = Arrays.stream(data).min().orElse(0);
        double max = Arrays.stream(data).max().orElse(1);
        double binWidth = (max - min) / numBins;

        int[] counts = new int[numBins];
        for (double value : data) {
            int binIndex = Math.min((int)Math.floor((value - min) / binWidth), numBins - 1);
            counts[binIndex]++;
        }

        Double[] binCenters = new Double[numBins];
        Double[] binLefts = new Double[numBins];
        Double[] binRights = new Double[numBins];
        double[] frequencies = new double[numBins];
        for (int i = 0; i < numBins; i++) {
            binCenters[i] = min + (i + 0.5) * binWidth;
            binLefts[i] = min + i*binWidth;
            binRights[i] = min + (i+1) * binWidth;
            frequencies[i] = counts[i] / (double)data.length;
        }

        FrequencyData frequency =  new FrequencyData();
        frequency.frequencies = frequencies;
        frequency.binLefts = binLefts;
        frequency.binRights = binRights;
        frequency.binCenters = binCenters;
        frequency.counts = counts;

        return frequency;
    }


    public Figure createBarChart(int numBins){
        //        DoubleColumn resultsColumn = DoubleColumn.create("Results", simulationResults);
//        Table dataTable = Table.create("Monte Carlo Simulation").addColumns(resultsColumn);
//        Table dataTable = Table.read().csv("10000.txt");
//        simulationResults = dataTable.doubleColumn("Results").asDoubleArray();
        FrequencyData freqData = calculateFrequencyData(dependentVariables, numBins);
        String[] name = new String[freqData.binCenters.length];
        for (int i=0;i<name.length;i++){
            name[i] = String.format("%.2f - %.2f", freqData.binLefts[i], freqData.binRights[i]);
        }
        // 4. 创建频率柱状图（左侧Y轴）
        BarTrace countBar = BarTrace.builder(name, Arrays.stream(freqData.counts).mapToDouble(i->(double) i).toArray())
                .name("Count")
                .marker(Marker.builder()
                        .color("#1f77b4")
                        .build())
                .yAxis("y")
                .build();

        BarTrace freqBarRight = BarTrace.builder(name, freqData.frequencies)
                .name("Frequency")
                .marker(Marker.builder()
                        .color("#ff7f0e")
                        .build())
                .yAxis("y2")
                .build();


        Layout layout3 = Layout.builder()
                .title("蒙特卡洛模拟结果分布 条形图 (分组数: " + numBins + ")")
                .xAxis(Axis.builder().title("模拟结果值").build())
                .yAxis(Axis.builder().title("频数").build())
                .yAxis2(Axis.builder().title("概率").side(Axis.Side.right).build())
                .width(800)
                .height(500)
                .build();

//        Plot.show(new Figure(layout, Config.builder().displayLogo(false).displayModeBar(false).responsive(false).build(), countBar));
//        Plot.show(new Figure(layout2, Config.builder().displayLogo(false).displayModeBar(false).responsive(false).build(), freqBar));
//        Plot.show(new Figure(layout3,  countBar, freqBarRight));
        Figure figure = new Figure(layout3, countBar, freqBarRight);
        if (showChart)
            Plot.show(figure);
        return figure;

//        Plot.show(new Figure(layout, Config.builder().displayLogo(false).displayModeBar(false).responsive(false).build(), trace, freqBar ));

//        Layout layout =
//                Layout.builder().title("Probability Histogram of Result").build();
//        HistogramTrace trace =
//                HistogramTrace.builder(dataTable.nCol("Results"))
//                        .histNorm(HistogramTrace.HistNorm.PROBABILITY).nBinsX(500)
//                        .build();

    }


    public Figure createHistogramChart(int numBins){


        Layout layout4 = Layout.builder()
                .title("蒙特卡洛模拟结果分布 直方图(箱数: " + numBins + ")")
                .xAxis(Axis.builder().title("模拟结果值").build())
                .yAxis(Axis.builder().title("频数").build())
                //.yAxis2(Axis.builder().title("概率").side(Axis.Side.right).build())
                .width(800)
                .height(500)
                .build();


        HistogramTrace histogramTrace = HistogramTrace.builder(dependentVariables).nBinsX(numBins).build();
        Figure figure = new Figure(layout4, histogramTrace);
        if (showChart)
            Plot.show(figure);
        return figure;
    }


    public Figure createHistogramChart(){
        Layout layout4 = Layout.builder()
                .title("蒙特卡洛模拟结果分布 直方图(箱数:auto)")
                .xAxis(Axis.builder().title("模拟结果值").build())
                .yAxis(Axis.builder().title("频数").build())
                //.yAxis2(Axis.builder().title("概率").side(Axis.Side.right).build())
                .width(800)
                .height(500)
                .build();


        HistogramTrace histogramTrace = HistogramTrace.builder(dependentVariables).build();
        Figure figure = new Figure(layout4, histogramTrace);
        if (showChart)
            Plot.show(figure);
        return figure;
    }
}
